# -*- coding: utf-8 -*-

import base64
import datetime
import getpass
import hashlib
import os
import pexpect
import pxssh
import settings
import sys
import tempfile
import thread
import utils
import web

from models import Communication, Crontab, File, FileMeta, FileAllow, FileDeny, Host, Resource, Tag, create_all
from multiprocessing import active_children, Pool, Process, Queue
try:
    from pygments import highlight
    from pygments.lexers import guess_lexer_for_filename
    from pygments.formatters import HtmlFormatter
except ImportError:
    pass

from sqlalchemy import create_engine, or_
from sqlalchemy.orm import eagerload, scoped_session, sessionmaker
from sqlalchemy.orm.exc import NoResultFound
from time import sleep
from web.background import background, backgrounder

if settings.DEBUG:
    if os.path.exists("%slogging.txt" % settings.DATA_DIRECTORY):
        import logging

        LOG_FILENAME = "%slogging.txt" % settings.DATA_DIRECTORY
        logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG,)

    #import warnings
    #warnings.simplefilter("error")
    

crawl_queue = Queue()

urls = (
    "/", "index",
    "/login", "login",
    "/logout", "logout",
    "/host/(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})", "HostDetail",
    "/host/add", "HostAdd",
    "/host/edit", "HostEdit",
    "/host/command/(.*)", "host_command",
    "/search", "Search",
    "/host/([0-9.]+)/explorer", "HostExplorer",
    "/file/([a-z0-9]+)", "FileDetail",
    "/file/base64/([a-z0-9]+)", "FileBase64Encoded",
    "/download/([a-z0-9]+)", "FileDownload",
    "/tag/add", "TagAdd",
    "/testa", "Testa",
)


app = web.application(urls, globals())

def load_sqla(handler):
    web.ctx.orm = scoped_session(sessionmaker(bind=engine))
    try:
        return handler()
    except web.HTTPError:
       web.ctx.orm.commit()
       raise
    except:
        web.ctx.orm.rollback()
        raise
    finally:
        web.ctx.orm.commit()

db_file_name = "%sdb.sqlite" % settings.DATA_DIRECTORY

if os.path.exists(db_file_name):
    engine = create_engine("sqlite:///%s" % db_file_name)
    app.add_processor(load_sqla)

def get_host_meta(db_session):
    return db_session.query(Meta).filter(Meta.id == 1).one()
    
def get_remote_directory_contents(ip, directory):
    ssh_c = pxsshConnection().login(ip)
    output = ssh_c.execute("ls -aBl --full-time --color=never %s" % directory)
    output = output.split("\n")
    del output[0] # Total XX
    return output

def like_grep(q, text):
    import re
    new_text = []
    i = 1
    if text is None:
        return ""
    else:
        for line in text.split("\n"):
            line = web.htmlquote(line).strip()
            if q.strip() != "" and q.lower() in line.lower():
                css_class = "line_q"
            else:
                css_class = "line"
            line = "<div class=\"%s\">%s</div>\n" % (css_class, line)
            new_text.append(line)
            i = i + 1
        return "\n".join(new_text)


def links_from_directory(directory):
    link = []
    previous = None
    sep = directory.split("/")
    for v in sep:
        href = v
        if previous is not None:
            href = v
        link.append('<a href="directories?dir=/%s">%s</a>' % (href, v))
        previous = v
    return "/".join(link)

def download_file_link(file_meta):
    if file_meta.type == "-":
        return '<a href="/download/%s" class="file">download</a>' % file_meta.hash
    # TODO: Isn't "l" for links? Why am I treating as a directory?
    elif file_meta.type == "l":
        return '<a href="/host/%s/explorer?directory=%s/%s" class="link">%s -> %s</a>' % (file_meta.file.host.ip, file_meta.file.path, file_meta.file.name, file_meta.file.name, file_meta.links_to)

def highlight_contents(file_meta):
    try:
        lexer = guess_lexer_for_filename(file_meta.file.name, file_meta.contents)
        formatter = HtmlFormatter(noclasses=True)
        result = highlight(file_meta.contents, lexer, formatter)
        return result
    except:
        return "<pre>%s</pre>" % file_meta.contents

def link_from_file_meta(file_meta):
    file = file_meta.file
    if file_meta.type == "-":
        return '<a href="/file/%s" class="file">%s</a>' % (file_meta.hash, file.name)
    elif file_meta.type == "d":
        if file.path == "/":
            path = ""
        else:
            path = file.path
        return '<a href="/host/%s/explorer?directory=%s/%s" class="directory">%s</a>' % (file.host.ip, path, file.name, file.name)
    # TODO: Isn't "l" for links? Why am I treating as a directory?
    elif file_meta.type == "l":
        return '<a href="/host/%s/explorer?directory=%s/%s" class="link">%s -> %s</a>' % (file.host.ip, file.path, file.name, file.name, file_meta.links_to)

def navigate_backwards(directory):
    # I am pretty sure there's a better way of doing this. 100% sure.
    sep = directory.split("/")
    backwards = []
    i = 1
    while i < len(sep):
        if i == 1:
            backwards.append("/")
        else:
            backwards.append("/".join(sep[:i]))
        i = i + 1
    backwards.append("/".join(sep))
    return backwards
    
def tag_list():
    return web.ctx.orm.query(Tag).distinct().order_by(Tag.tag)

render = web.template.render("templates/")

web.template.Template.globals.update(dict(
    render=render,
    download_file_link=download_file_link,
    highlight_contents=highlight_contents,
    like_grep=like_grep,
    link_from_file_meta=link_from_file_meta,
    navigate_backwards=navigate_backwards,
    tag_list=tag_list,
    size_h=utils.size_h,
))

session_dir = tempfile.mkdtemp()
# web.config.debug = False
# session = web.session.Session(app, web.session.DiskStore(session_dir))
if web.config.get("_session") is None:
    session = web.session.Session(app, web.session.DiskStore(session_dir))
    web.config._session = session
else:
    session = web.config._session

# http://code.activestate.com/recipes/52558/#c10
class pxsshConnection(object):
    """Implement Pattern: SINGLETON """

    __lockObj = thread.allocate_lock() # lock object
    __instance = None # the unique instance

    def __new__(cls, *args, **kargs):
        return cls.getInstance(cls, *args, **kargs)

    def getInstance(cls, *args, **kargs):
        """Static method to have a reference to **THE UNIQUE** instance"""
        # Critical section start
        cls.__lockObj.acquire()
        try:
            if cls.__instance is None:
                # (Some exception may be thrown...)
                # Initialize **the unique** instance
                # cls.__instance = object.__new__(cls, *args, **kargs) # Deprecation warning, __new__() takes no arguments
                cls.__instance = object.__new__(cls)

                '''Initialize object **here**, as you would do in __init__()...'''
        finally:
            #  Exit from critical section whatever happens
            cls.__lockObj.release()
        # Critical section end

        return cls.__instance
    getInstance = classmethod(getInstance)

    def __init__(self):
        pass

    def login(self, ip):
        host = web.ctx.orm.query(Host).filter(Host.ip==ip).one()
        if not hasattr(self, "_connected") or not self._connected or self._connected != ip:
            try:
                self._s = pxssh.pxssh(logfile=utils.logfile(ip))
                self._s.login(ip, host.username, host.password)
                self._connected = ip
                # Commands can get truncated because of terminal size, rendering return .replace on self.execute() useless
                # If we send for example "ls -and -a -bunch -of -other -parameters /on/some/directory", what really happens is
                # login@host:/some/dir$ ls -and -a -bunch -of -other -parameters /on/some/directory
                # Which has 82 caracters, above the usual 80 (type "stty size" on a terminal to see yours)
                # And therefore, for a ls, self._s.before will be something like (notice the carriage return 2 characters before the end of line
                # ls -and -a -bunch -of -other -parameters /on/some/directo\rry
                # total 44
                # ... rest of ls
                # And if we print self._s.before, this is what appears:
                # ss -aBl --full-time --color=never /home/notroot/dev/proxpect/template
                # total 44
                # ... rest of ls
                # So we increase the terminal size!
                # 1000 is pretty random. Will we ever send a command larger than 1000 characters? Should I increase it dinamically based on len(command)?
                self._s.sendline("stty rows 24 cols 1000")
                self._s.prompt()
                print "connecting to " + ip
                web.ctx.orm.add(Communication(host, True, datetime.datetime.today()))
                output = self.execute("crontab -l")
                output_hash = hashlib.md5(output).hexdigest()
                try:
                    crontab = web.ctx.orm.query(Crontab).filter_by(hash=output_hash).one()
                except NoResultFound:
                    crontab = None
                if crontab is None:
                    web.ctx.orm.add(Crontab(output_hash, host, output, datetime.datetime.today()))
            except pxssh.ExceptionPxssh, e:
                self._connected = False
                print "pxssh failed on login."
                print str(e)
                web.ctx.orm.add(Communication(host, False, datetime.datetime.today()))
        else:
            print "already connected to " + str(self._connected)
            web.ctx.orm.add(Communication(host, True, datetime.datetime.today()))        
        return self

    def execute(self, command):
        self._s.sendline(command) # TODO: cat /etc/lsb-release works better on Ubuntu (and probably lsb distros?)
        self._s.prompt()
        return self._s.before.replace(command, "").strip() 

    def logout(self):
        s.logout()

def get_files_meta_hash(host):
    files_meta = set()
    current_files_meta = web.ctx.orm.query(FileMeta).filter(File.host==host).all()
    for file in current_files_meta:
        files_meta.add(file.hash)
    return files_meta

def login_required(f):
    def wrapped(*args, **kargs):
        if not "loggedin" in session:
            raise web.seeother("/login")
        return f(*args, **kargs)
    return wrapped

def host_list():
    return [host.ip for host in web.ctx.orm.query(Host.ip).order_by(Host.ip)]

def secret_word():
    f = open("%s.secret_word" % settings.DATA_DIRECTORY)
    data = f.read()
    f.close()
    return data
    
class FileBase64Encoded:
    @login_required
    def GET(self, hash):
        file = web.ctx.orm.query(File).filter_by(hash=hash).one()

        if file.contents is not None:
            if file.contents == "":
                print "não vazio"
        
        
        # We have the file on the db
        if file.contents is not None and file.contents != "":
            contents = file.contents
        # We don't have it... download and serve its contents
        else:
            file.download()
            contents = open("data/files/%s" % file.name).read()
            file.remove_local()
            
        return "data:%s;base64,%s" % (file.mime_type, base64.b64encode(contents))


class FileDetail:
    @login_required
    def GET(self, hash):
        file_meta = web.ctx.orm.query(FileMeta).filter_by(hash=hash).one()
        other_versions = web.ctx.orm.query(FileMeta).filter_by(file_id=file_meta.file_id).order_by(FileMeta.last_modified).all()
        return render.file_detail(file_meta, other_versions)
        
        
class FileDownload:
    @login_required
    def GET(self, hash):
        file_meta = web.ctx.orm.query(FileMeta).filter(FileMeta.hash==hash).one()

        # We have the file on the db
        if file_meta.contents is not None and file_meta.contents != "":
            contents = file.contents
        # We don't have it... download and serve its contents
        else:
            file_meta.file.download()
            contents = open("data/files/%s" % file_meta.file.name).read()
            file_meta.file.remove_local()

        # http://php.net/manual/en/function.header.php#83384
        web.header("Pragma", "public");
        web.header("Expires", "0");
        web.header("Cache-Control", "must-revalidate, post-check=0, pre-check=0");

        web.header("Content-Type", "application/force-download");
        web.header("Content-Type", "application/octet-stream");
        web.header("Content-Type", "application/download");

        # Double quotes around file name needed it it has spaces
        # http://php.net/manual/en/function.header.php#87449
        web.header("Content-Disposition", 'attachment; filename="%s";' % file_meta.file.name);

        web.header("Content-Transfer-Encoding", "binary");
        web.header("Content-Length", file_meta.size);
        return contents


class HostAdd:
    def POST(self):
        i = web.input()
        host = Host(i.ip)
        web.ctx.orm.add(host)
        raise web.seeother("/")


class HostDetail:
    @login_required
    def GET(self, ip):
        host = web.ctx.orm.query(Host).filter(Host.ip==ip).one()
        return render.host_detail(host_list(), host)


class HostEdit:
    @login_required
    def POST(self):
        i = web.input()
        host = web.ctx.orm.query(Host).filter(Host.ip==i.ip).one()
        host.username = i.username
        host.password = i.password
        if "sudo" in i:
            host.sudo = True
        else:
            host.sudo = False
        web.ctx.orm.add(host)
        raise web.seeother("/host/%s" % i.ip)


class HostExplorer:
    @login_required
    def GET(self, ip):
        i = web.input()
        host = web.ctx.orm.query(Host).filter_by(ip=ip).one()

        ssh_c = pxsshConnection().login(ip)

        if web.input().has_key("directory"): # Specified directory
            current_directory = i.directory
        else: # Wherever we go when logged in by ssh
            current_directory = ssh_c.execute("pwd").split().pop()

        if not isinstance(current_directory, unicode):
            current_directory = unicode(current_directory, "utf-8")
            
        current_files = {}
        for file in host.files:
            current_files["%s/%s" % (file.path, file.name)] = file
        
        current_files_meta = get_files_meta_hash(host)

        output = get_remote_directory_contents(ip, current_directory)
        
        for o in output:
            partes = o.split()
            if o[0] == "-" or o[0] == "d":
                if len(partes) == 9:
                    mode, links, owner, group, size, Ymd, time, tz, name =  partes
                else:
                    # http://www.nabble.com/Re%3A-Is-there-any-nice-way-to-unpack-a-list-of-unknown-size---p19483627.html
                    mode, links, owner, group, size, Ymd, time, tz =  partes[:8]
                    name = " ".join(partes[8:])

                links_to = None
            elif o[0] == "l":
                mode, links, owner, group, size, Ymd, time, tz, name, arrow, links_to =  partes
            if name == "." or name == "..":
                continue

            path_name = current_directory + "/" + name
            file_type = mode[0]
            permission = mode[1:]
            Y, m, d = Ymd.split("-")
            H, i, s = time.split(":")
            last_modified = datetime.datetime(int(Y), int(m), int(d), int(H), int(i), int(s[0:2]))

            hash = utils.file_hash(host.ip, current_directory, name, permission, str(last_modified))
            
            if path_name in current_files:
                file = current_files[path_name]
            else:
                file = File(host, current_directory, name)                

            if hash not in current_files_meta:
                file_meta = FileMeta(file, file_type, links_to, permission, owner, group, last_modified)
                file_meta.hash = hash
                file_meta.crawl()
                # TODO: Download the files in parallel, limiting to 4
                #p_crawl = Process(target=file.crawl)
                #p_crawl.start()                
                #p_crawl.join()
        # Get the latest files (order by last modified and group by name)
        files_meta = web.ctx.orm.query(FileMeta).join(File).filter(File.path==current_directory).order_by(File.name).group_by(FileMeta.file_id).all()
        return render.host_explorer(current_directory, files_meta)


class Testa:
    @backgrounder
    def GET(self):
        print "Started!"
        print "hit f5 to refresh!"
        longrunning()


@background
def longrunning():
    for i in range(10):
        sleep(1)
        print "%s: %s" % (i, now())


class Search:
    @login_required
    def GET(self):
        i = web.input()
        q = i.q.strip()
        tag = i.tag.strip()
        if q == "" and tag == "":
            raise web.seeother("/")
        query = web.ctx.orm.query(FileMeta).join(File)
        if q != "":
            query = query.filter(or_(FileMeta.contents.ilike("%" + i.q + "%"),
                                     File.name.ilike("%" + i.q + "%")
                                     ))
        if tag != "":
            query = query.join(Tag).filter(Tag.tag==tag)
        results = query.order_by(File.name).order_by(FileMeta.last_modified.desc()).all()
        return render.search(i.q, results)


class TagAdd:
    @login_required
    def POST(self):
        i = web.input()
        try:
            file = web.ctx.orm.query(File).filter_by(id=i.id).one()
        except NoResultFound:
            return None
        tag = Tag(file, i.tag)
        web.ctx.orm.add(tag)
        return i.tag


class index:
    @login_required
    def GET(self):
        return render.index(host_list())


class login:
    def GET(self):
        if "loggedin" in session:
            raise web.seeother("/")
        return render.login()

    def POST(self):
        if "loggedin" in session:
            raise web.seeother("/")
        i = web.input()
        if hashlib.sha512(i.secret_word).hexdigest() == secret_word():
            session.loggedin = True
            raise web.seeother("/")
        else:
            raise web.seeother("/login")

class logout:
    def GET(self):
        session.kill()
        raise web.seeother("/")

if __name__ == "__main__":
    if not os.path.exists(settings.DATA_DIRECTORY):
        os.mkdir(settings.DATA_DIRECTORY)
        
    if not os.path.exists("%sfiles" % settings.DATA_DIRECTORY):
        os.mkdir("%sfiles" % settings.DATA_DIRECTORY)
        

    # Ask user to create a secret word (to access the app).
    # This should only happen once, the first time Proxpect runs.
    # LOW LAYER OF SECURITY FTW! It's not actuallty *that* bad, but I should
    # warn the users that the point here is not to provide the best possible
    # security ever. It's just one layer. They should SSH properly and SUDO
    # and IPTABLES the whole thing.
    if os.path.exists("%s.secret_word" % settings.DATA_DIRECTORY) and os.path.exists(db_file_name):
        p_app_run = Process(target=app.run)
        p_app_run.start()
        
        # session = scoped_session(sessionmaker(bind=engine))
        
        # def get_uncrawled():
            # return session.query(File).filter_by(type="d", crawled=False).order_by(File.name, File.last_modified).group_by(File.name).all()

        # def crawl(file):
            # file.crawl()
            # session.commit()
            # print file
        
        # uncrawled_files = get_uncrawled()

        # for file in uncrawled_files:
            # Process(target=crawl, args=(file,)).start()
            # sleep(0.5)
        
    else:
        secret_word = ""
        print "It looks like this is the first time you're running Proxpect. Please set the secret word, which will be used to access Proxpect.\n"
        try:
            while 1:
                if not secret_word:
                    secret_word = getpass.getpass("Secret word: ")
                    secret_word2 = getpass.getpass("Secret word (again): ")
                    if secret_word != secret_word2:
                        sys.stderr.write("Error: Your secret words didn't match.\n")
                        secret_word = None
                        continue
                if secret_word.strip() == "":
                    sys.stderr.write("Error: Blank secret words aren't allowed.\n")
                    secret_word = None
                    continue
                break
        except KeyboardInterrupt:
            sys.stderr.write("\nOperation cancelled.\n")
            sys.exit(1)

        # Store the secret word (hashed)
        fp = open("%s.secret_word" % settings.DATA_DIRECTORY, "wb")
        fp.write(hashlib.sha512(secret_word).hexdigest())
        fp.close()
        engine = create_engine("sqlite:///%s" % db_file_name)
        create_all(engine)
        print "You're good to go. Start the app again."

